

package com.googlecode.flac4j.format.frame.sub;


import com.googlecode.flac4j.format.FormatHelper;

import com.googlecode.jinahya.io.BitInput;
import com.googlecode.jinahya.io.BitOutput;

import java.io.IOException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;


/**
 *
 * @author Jin Kwon <jinahya at gmail.com>
 * @param <P> partition type parameter
 */
@XmlTransient
@XmlType(propOrder = {"order", "partitions"})
public abstract class AbstractRice<P extends Partition> extends Rice {


    /**
     * Creates a new instance.
     *
     * @param residualMethod residual coding method
     * @param partitionType partition type
     * @param orderLength order length in bits
     */
    public AbstractRice(final int residualMethod, final Class<P> partitionType,
                        final int orderLength) {
        super();

        if (residualMethod >> 2 != 0) {
            throw new IllegalArgumentException(
                "method(" + residualMethod + ") >> 2 != 0");
        }

        if (partitionType == null) {
            throw new NullPointerException("null partitionType");
        }

        if (orderLength < 1) {
            throw new IllegalArgumentException(
                "orderLength(" + orderLength + ") < 1");
        }

        if (orderLength >= 32) {
            throw new IllegalArgumentException(
                "orderLength(" + orderLength + ") >= 32");
        }

        this.residualMethod = residualMethod;

        this.partitionType = partitionType;

        this.orderLength = orderLength;
        maximumPartitionCount = (int) Math.pow(2, this.orderLength) - 1;
    }


    @Override
    public void read(final BitInput input) throws IOException {

        order = input.readUnsignedInt(orderLength);

        partitions.clear();
        final int partitionCount = (int) Math.pow(2, order);
        for (int i = 0; i < partitionCount; i++) {
            try {
                final P partition = partitionType.newInstance();
                partition.setRice(this);
                partition.read(input);
                partitions.add(partition);
            } catch (InstantiationException ie) {
                throw new RuntimeException(
                    "failed to create a new instance of " + partitionType, ie);
            } catch (IllegalAccessException iae) {
                throw new RuntimeException(
                    "failed to create a new instance of " + partitionType, iae);
            }
        }
    }


    @Override
    public void write(final BitOutput output) throws IOException {

        /*
        if (partitions.isEmpty()) {
        throw new IllegalStateException("empty partitions");
        }
         */

        order = FormatHelper.log2(partitions.size());
        if (partitions.size() % Math.pow(2, order) != 0) {
            throw new IllegalStateException(
                "partitions.size(" + partitions.size()
                + ") is not any Nth power of 2");
        }

        output.writeUnsignedInt(4, order);

        for (P partition : partitions) {
            partition.write(output);
        }
    }


    @Override
    public final int getResidualMethod() {
        return residualMethod;
    }


    // ------------------------------------------------------------------- order
    @Override
    public int getOrder() {
        return order;
    }


    @Override
    public void setOrder(final int order) {

        if (order >> orderLength != 0) {
            throw new NullPointerException(
                "order(" + order + ") >> orderLength(" + orderLength
                + ") != 0");
        }

        this.order = order;
    }


    // -------------------------------------------------------------- partitions
    /**
     * Clears partitions.
     */
    public void clearPartitions() {
        partitions.clear();
    }


    /**
     * Returns the number of partitions added so far.
     *
     * @return number of partitions
     */
    public int getPartitionCount() {
        return partitions.size();
    }


    /**
     * Adds given <code>partition</code>.
     *
     * @param partition partition
     */
    public void addPartition(final P partition) {

        if (partition == null) {
            throw new NullPointerException("null partition");
        }

        if (partitions.size() >= maximumPartitionCount) {
            throw new IllegalStateException("can't add moer partitions");
        }

        partition.setRice(this);

        partitions.add(partition);
    }


    /**
     * Adds given <code>partition</code> at specified <code>index</code>.
     *
     * @param index index partition index
     * @param partition partition to add
     */
    public void addPartition(final int index, final P partition) {

        if (partition == null) {
            throw new NullPointerException("null partition");
        }

        partitions.add(index, partition);
    }


    /**
     * Returns an unmodifiable list of partitions.
     *
     * @return an unmodifiable list of partitions.
     */
    public List<P> getPartition() {

        return Collections.unmodifiableList(partitions);
    }


    /** target residual coding method. */
    @XmlTransient
    private final int residualMethod;


    /** partition type. */
    @XmlTransient
    private final Class<P> partitionType;


    /** order length in bits. */
    @XmlTransient
    private final int orderLength;


    /** maximum number of partitions. */
    @XmlTransient
    private final int maximumPartitionCount;


    /** partition order. */
    @XmlElement(required = true)
    private int order;


    /** partitions. */
    @XmlElement(name = "partition")
    @XmlElementWrapper(required = true)
    private final List<P> partitions = new ArrayList<P>();


}

